package apple.core;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;

import apple.ui.IMonitor;

/**
 * @author Christiaan
 *
 */
public class Apple {

   public static final String APPLICATION_TITLE = "Apple II Emulator";

   private KeyboardDecoder keyboardDecoder;

   private IMonitor monitor;

   private MOS6502 cpu;

   private Memory memory;

   private VideoGenerator video;

   private Speaker speaker;

   private DiskII disk;

   static Logger logger = Logger.getLogger(Apple.class);

   private Thread videoThread;

   private Thread cpuThread;

   public enum Status {
      SHUTDOWN, RUNNING, SUSPENDED
   };

   private Status status = Status.SHUTDOWN;

   private String name;

   public static final String PROPERTY_NAME = "statusChange";

   private List<PropertyChangeListener> listeners;

   public Apple(String name) throws IOException {
      this.name = name;
      keyboardDecoder = new KeyboardDecoder();
      video = new VideoGenerator();
      speaker = new Speaker();
      disk = new DiskII();
      memory = new Memory(keyboardDecoder, video, speaker, disk);
      cpu = new MOS6502(memory);
      memory.initialize("C:\\temp\\APPLE2.ROM");
      disk.insertDisk("C:\\temp\\Dos3.3_master.dsk");
      listeners = new ArrayList<PropertyChangeListener>();
   }

   public String getName() {
      return name;
   }

   public IMonitor getMonitor() {
      return monitor;
   }

   public void setMonitor(IMonitor monitor) {
      this.monitor = monitor;
      video.setMonitor(monitor);
   }

   public KeyboardDecoder getKeyboardDecoder() {
      return keyboardDecoder;
   }

   public String toString() {
      return getName();
   }

   public Status getStatus() {
      return status;
   }

   /**
    * Starts up all components of the system.
    * 
    * This method should not block; it should start any long-running operations
    * in separate threads and return immediately.
    */
   public void startup() {

      /*
       * This method needs to reset the components of the system in order to
       * emulate a cold-start.
       */

      assert status == Status.SHUTDOWN : String.format(
            "incompatible status: %s", status);
      Status oldStatus = status;

      Runnable videoRunnable = new Runnable() {
         public void run() {
            video.run();
            logger.debug(String.format("Thread \"%s\" finished", Thread
                  .currentThread().getName()));
         }
      };
      videoThread = new Thread(videoRunnable, "Video Generator");
      videoThread.start();

      Runnable cpuRunnable = new Runnable() {
         public void run() {
            cpu.reset();
            cpu.run();
            logger.debug(String.format("Thread \"%s\" finished", Thread
                  .currentThread().getName()));
         }
      };
      cpuThread = new Thread(cpuRunnable, "CPU");
      cpuThread.start();

      status = Status.RUNNING;
      PropertyChangeEvent event = new PropertyChangeEvent(this, PROPERTY_NAME,
            oldStatus, status);
      firePropertyChange(event);
   }

   /**
    * Shuts down all components of the system.<br>
    * <br>
    * This method should block until all threads started by startup() have
    * exited. This shutdown should occur very quickly since this method may be
    * called on the UI thread.<br>
    * <br>
    * The intention is that this method will be called <b>prior</b> to the
    * shutdown of the GUI system in order to ensure that threads do not attempt
    * to interact with GUI widgets which otherwise may have been disposed.
    */
   public void shutdown() {
      assert status == Status.RUNNING || status == Status.SUSPENDED : String
            .format("incompatible status: %s", status);
      Status oldStatus = status;
      boolean shutdown = false;
      cpu.halt();
      video.halt();
      while (!shutdown) {
         try {
            cpuThread.join();
            videoThread.join();
            shutdown = true;
         } catch (InterruptedException e) {
            logger.debug(e);
         }
      }
      cpuThread = null;
      videoThread = null;
      status = Status.SHUTDOWN;
      PropertyChangeEvent event = new PropertyChangeEvent(this, PROPERTY_NAME,
            oldStatus, status);
      firePropertyChange(event);
   }

   public void suspend() {
      assert status == Status.RUNNING : String.format(
            "incompatible status: %s", status);
      Status oldStatus = status;
      cpu.suspend();
      video.suspend();
      status = Status.SUSPENDED;
      PropertyChangeEvent event = new PropertyChangeEvent(this, PROPERTY_NAME,
            oldStatus, status);
      firePropertyChange(event);
   }

   public void resume() {
      assert status == Status.SUSPENDED : String.format(
            "incompatible status: %s", status);
      Status oldStatus = status;
      video.resume();
      cpu.resume();
      status = Status.RUNNING;
      PropertyChangeEvent event = new PropertyChangeEvent(this, PROPERTY_NAME,
            oldStatus, status);
      firePropertyChange(event);
   }

   public void addPropertyChangeListener(PropertyChangeListener listener) {
      if (!listeners.contains(listener)) {
         listeners.add(listener);
      }
   }

   public void removePropertyChangeListener(PropertyChangeListener listener) {
      listeners.remove(listener);
   }

   protected void firePropertyChange(PropertyChangeEvent event) {
      for (PropertyChangeListener listener : listeners) {
         listener.propertyChange(event);
      }
   }
}
